﻿using System; 
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.XPath;

using Microsoft.Office.InfoPath;
using System.Xml;

namespace InfoPathDebugger
{
    /// <summary>
    /// Provide a set of faciliate methods to manipulate xpath objects.
    /// </summary>
    public class XPathHelper
    {
        public static String GetNodeValue(XPathNavigator navigator, String xpath, String namespacePrefix)
        {
            XPathNavigator tempNavigator = navigator.CreateNavigator();
            String nodeValue = String.Empty;
            if (String.IsNullOrEmpty(namespacePrefix))
            {
                nodeValue = tempNavigator.SelectSingleNode(xpath).Value;
            }
            else
            {
                XmlNamespaceManager nsManager = new XmlNamespaceManager(navigator.NameTable);
                nsManager.AddNamespace(namespacePrefix, RetrieveNamespaces(tempNavigator)[namespacePrefix]);

                nodeValue = tempNavigator.SelectSingleNode(xpath, nsManager).Value;
            }

            return nodeValue;
        }

        private static void deleteNil(XPathNavigator node)
        {
            XPathNavigator tempNode = node.CreateNavigator();
            if (tempNode.MoveToAttribute("nil", "http://www.w3.org/2001/XMLSchema-instance"))
            {
                tempNode.DeleteSelf();
            }
        }

        /// <summary>
        /// Set a value to a specific node.
        /// </summary>
        /// <returns>true - success, false - failed.</returns>
        public static Boolean SetNodeValue(XPathNavigator navigator, String xpath, String value, String namespacePrefix)
        {
            XPathNavigator tempNavigator = navigator.CreateNavigator();
            XPathNavigator nodeNav = null;

            // If the namespace prefix is empty, select the node directly. Otherwise, find the namespace first,
            // then select the node.
            if (String.IsNullOrEmpty(namespacePrefix))
            {
                nodeNav = tempNavigator.SelectSingleNode(xpath);
            }
            else
            {
                XmlNamespaceManager nsManager = new XmlNamespaceManager(navigator.NameTable);
                nsManager.AddNamespace(namespacePrefix, RetrieveNamespaces(tempNavigator)[namespacePrefix]);

                nodeNav = tempNavigator.SelectSingleNode(xpath, nsManager);
            }

            // If the node can't be found, return false.
            if (nodeNav == null)
            {
                return false;
            }

            // Delete the nil attribute or else it'll throw an error.
            deleteNil(nodeNav);

            nodeNav.SetValue(value);
            return true;
        }

        /// <summary>
        /// Retrieve all namespaces of an xml document.
        /// </summary>
        /// <param name="navigator">A navigator of the xml document.</param>
        /// <returns>A dictionary contains prefix to namespace uri.</returns>
        public static IDictionary<String, String> RetrieveNamespaces(XPathNavigator navigator)
        {
            XPathNavigator dsNav = navigator.CreateNavigator();
            dsNav.MoveToFollowing(XPathNodeType.Element);
            return dsNav.GetNamespacesInScope(System.Xml.XmlNamespaceScope.All);
        }

        /// <summary>
        /// Gets all xpaths of leaf nodes of a data source.
        /// </summary>
        /// <param name="ds">A navigator points to the root of a data source.</param>
        /// <returns>A list represents all xpaths of leaf nodes of the specific data source.</returns>
        public static List<String> GetDatasourceLeafNodesXPath(XPathNavigator dsNav)
        {
            List<String> leafNodesXPath = new List<string>();
            StringBuilder sb = new StringBuilder();
            if (!String.IsNullOrEmpty(dsNav.Name))
            {
                sb.Append("/" + dsNav.Name);
            }

            recursiveFindLeafNodeAndExtractXPath(dsNav, ref leafNodesXPath, sb);

            return leafNodesXPath;
        }

        /// <summary>
        /// Determine whether the node has child elements.
        /// </summary>
        /// <param name="navigator">Specifies a navigator points to the node.</param>
        /// <returns>true if the node has child, otherwise false.</returns>
        public static Boolean HasChildElement(XPathNavigator navigator)
        {
            XPathNavigator tempNav = navigator.Clone();
            return tempNav.MoveToChild(XPathNodeType.Element);
        }

        /// <summary>
        /// Create a string contains the xpath of a specific navigator.
        /// </summary>
        /// <param name="navigator">A navigator to create xpath string.</param>
        /// <returns>XPath string of the navigator.</returns>
        public static String CreateXPathString(XPathNavigator navigator)
        {
            StringBuilder path = new StringBuilder();

            // Get the ancestors of the navigator.
            XPathNodeIterator iter = navigator.SelectAncestors(XPathNodeType.Element, false);

            // Go througn all ancestors to build the xpath string.
            path.Insert(0, iter.Current.Name);
            path.Insert(0, "/");

            while (iter.MoveNext())
            {
                path.Insert(0, iter.Current.Name);
                path.Insert(0, "/");
            }

            return path.ToString();
        }

        private static void recursiveFindLeafNodeAndExtractXPath(XPathNavigator navigator, ref List<String> nodeXPaths, StringBuilder currentNodeXPath)
        {

            // If the root node has children, move to the child node.
            if (navigator.MoveToChild(XPathNodeType.Element))
            {
                // Store curernt node xpath, this value will be used to find next sibling node.
                StringBuilder path = new StringBuilder(currentNodeXPath.ToString());

                // Clone a navigator. The original one will be used to find next sibling node.
                XPathNavigator nav = navigator.Clone();

                currentNodeXPath.Append("/");
                currentNodeXPath.Append(nav.Name);
                recursiveFindLeafNodeAndExtractXPath(nav, ref nodeXPaths, currentNodeXPath);

                // If current node has sibling nodes, find the leaf child node of the next node.
                while (navigator.MoveToNext(XPathNodeType.Element))
                {
                    XPathNavigator tempNav = navigator.Clone();
                    StringBuilder tempPath = new StringBuilder(path.ToString());
                    tempPath.Append("/");
                    tempPath.Append(tempNav.Name);
                    recursiveFindLeafNodeAndExtractXPath(tempNav, ref nodeXPaths, tempPath);
                }
            }
            else
            {
                // Add the leaf node xpath to the list.
                String currentXPath = currentNodeXPath.ToString();
                if (!String.IsNullOrEmpty(currentXPath))
                {
                    nodeXPaths.Add(currentXPath);
                }
            }
        }
    }
}
